<?

/** A soSet class and extra functions for it.
 *  This collection of functions (libary) is licensed under GPL2.
 *  See LICENSE or www.gnu.org for more details.
 *  @author Jyry Kuukkanen
 */

include_once("sobaseclass.php");
include_once("sogeneric.php");
include_once("sostring.php");
include_once("sodebug.php");


/** Named list.
 *  Similar to soList, but uses "key" instead of index no. to store and
 *  retrieve items from the list.
 *  @var array/any $Keys Keys to set and get values from the list.
 *  @var array/any $Values Values to assign using keys
 *  @var $Debug This can be set from out side class and can be used for any
 *      debugging purposes.
 */
class soSet extends soBaseClass {

    /** @access private */
    var $Keys;
    var $Values;
    var $Debug;


    /** Constructor
     *  @param NULL||array $InArray Optional associative array to import
     *  values from.
     */
    function soSet($InArray = NULL) {
        $this->soBaseClass();
        $this->Keys = array();
        $this->Values = array();
        if (is_array($InArray)) $this->joinArray($InArray);
    } // constructor


    /** Adds an item to the list.
     *  Adds an specified element for specified key.
     *  @param string $Key Key to add an element with.
     *  @param any $Value The item to add with the key.
     *  @return bool True(1) or false(0) on error (when specified key already
     *      exists).
     *  @package soset
     */
    function addItem($Key, $Value = '') {
        return $this->setItem($Key, $Value, 0);
    } // addItem

    /** Inserts an item value to the set.
     *  Inserts a value by specified key before specified place key.
     *  @param string $PlaceKey Key to insert the value before - when NULL,
     *      inserts as last.
     *  @param string $Key Key to insert the value by.
     *  @param any $Value The actual value to store.
     *  @return bool True(1) or false(0) on error: a) PlaceKey does not exist,
     *      b) Key already exists
     *  @package soset
     */
    function insItem($PlaceKey, $Key, $Value) {
        settype($Key, "string");
        if (soIsNull($PlaceKey)) {
            $pos = -1;
        } else {
            settype($PlaceKey, "string");
            $pos = soArrayFind($this->Keys, $PlaceKey, 1);
            if (($pos == -1) ||
                (in_array($Key, $this->Keys))) return 0; // <-- EXIT
        }; // else if

        soInsElement($this->Keys, $Key, $pos);
        soInsElement($this->Values, $Value, $pos);
    } // insItem


    /** Get item.
     *  Returns an item by key. The value can be forced to be returned in
     *      desired datatype.
     *  @param string $Key Name of the item.
     *  @param string $AsType One of "any", "bool" (returns 1/0), "int"
     *  (intval) or "dec" (doubleval).
     *  @return any Returns an item by specified key or NULL if no such item
     *      exists (also Error property is set to true(1).
     *  @package soset
     */
    function getItem($Key, $AsType = 'any') {
        settype($Key, "string");
        $pos = $this->_getItemPos($Key);
        if ($pos != -1) {
            if ($this->Debug) soDebug('soSet->GetItem('.$Key.', '.$AsType.
                                      '): '.$pos.'='.$this->Values[$pos],
                                      SOD_DET);
            $this->Error = 0;
            $result = $this->Values[$pos];
        } else {
            if ($this->Debug) soDebug('soSet->GetItem('.$Key.', '.$AsType.
                                      '): not found', SOD_DET);
            $this->Error = 1;
            $result = NULL;
        }; // if

        return soGetAsType($result, $AsType);
    } // getItem


    /** Get one or more items as an array.
     *  @param array/string $keys An array of items to get.
     *  @param string $AsType One of "any", "bool" (returns 1/0), "int"
     *  (intval) or "dec" (doubleval).
     *  @return array/any Items requested. Items not found are boldly skipped.
     */
    function getItems($Keys, $AsType = 'any') {
        $result = array();
        if (!is_array($Keys)) return $result; // <-- EXIT

        foreach ($Keys as $key) {
            $item = $this->getItem($key, $AsType);
            if (!$this->Error) $result[] = $item;
        }; // foreach
        $this->Error = 0;
        return $result;
    } // getItems


    /** Assigns a key to a value.
     *  @param string $Key Key to the value.
     *  @param any $Value Value to assign to the key
     *  @param int $Replace 0=skip assignment when item already exists, 1=replace is
     *      existing value, if any, 2=clear the whole set before assignment
     *  @return bool Returns true(1) -- or false(0) if replace option is not
     *      set and key already exists.
     *  @package soset
     */
    function setItem($Key, $Value, $Replace = 1) {

        settype($Key, "string");
        if ($Replace == 2) {
            $this->clear();
            $pos = -1;
        } else {
            $pos = $this->_getItemPos($Key);
        };

        if ($pos == -1) {
            $this->Keys[] = $Key;
            $this->Values[] = $Value;
            $result = 1;
        } else {
            if ($Replace) {
                $this->Values[$pos] = $Value;
                $result = 1;
            } else {
                $result = 0;
            };
        }; // else if

        return $result;
    } // setItem


    /** Renames a set item.
     *  @param string $OldName Item key to rename
     *  @param string $NewName New key name.
     *  @return bool Returns true(1) unless item to be renamed does not exit when
     *  return false(0).
     *  @package soset
     */
    function renameItem($OldName, $NewName) {
        settype($OldName, "string");
        settype($NewName, "string");
        $pos = $this->_getItemPos($OldName);
        if ($pos == -1) return 0; // <-- EXIT

        $this->Keys[$pos] = $NewName;
        return 1;
    } // renameItem


    /** Joins a set to this one.
     *  @param soSet $Set Set to join.
     *  @param bool $Replace When set(1), then existing values in this set are
     *      replaced by the one in joined set - otherwise duplicate values are
     *      skipped.
     *  @return int Returns the number of the keys added or -1 on error, such as the
     *      set is not of type soSet.
     *  @package soset
     */
    function joinSet($Set, $Replace = 0) {
        if ((gettype($Set) != 'object') || 
            (strtolower(soGetClass($Set)) != 'soset')) return -1;

        if ($Set->size() == 0) return 0; // <-- EXIT

        $result = 0;
        $keys = $Set->getKeys();
        $kcount = count($keys);
        for ($k = 0; $k < $kcount; $k++) {        
            if (($Replace) ||
                (!$this->itemExists($keys[$k]))) {
                $this->setItem($keys[$k], $Set->getItem($keys[$k]));
                $result++;
            };
        } // for $k

        return $result;
    } // joinSet


    /** Returns keys from the set.
     *  Returns specified range or all keys in the set as an array
     *  @param int $Start First key to return, 0=1st in the set.
     *  @param int $End Last key to return, -1=last in the set.
     *  Returns the key fro specified range as an array. On error returns an
     *  empty array.
     *  @package soset
     */
    function getKeys($Start = 0, $End = -1) {
        $keys = array_values($this->Keys);

        /* The default range is so common, that there is no point going
           through the slow procedure. */
        if (($Start == 0) && ($End == -1)) return $keys; // <-- EXIT

        $kcount = count($keys);
        if ($End == -1) $End = $kcount -1;
        $result = array();
        if (($Start < 0) || ($End < -1) || 
            ($End >= $kcount) || ($Start > $End)) return $result; // <-- EXIT

        for ($k = $Start; $k <= $End; $k++) {
            $result[] = $keys[$k];
        }; // for

        return $result;
    } // getKeys


    /** Returns keys from the set as string.
     *  Equal to getKeys(), but takes one mandatory parameter that is used as a
     *  delimiter char/string. This method is only for convenience, it's just a
     *  wrapper for getKeys().
     *  @param string $Delim Delimiter char/string to separate on key from
     *      another.
     *  @param int $Start First key to return, 0=1st in the set.
     *  @param int $End Last key to return, -1=last in the set.
     *  Returns the key fro specified range as a string. On error returns an
     *  empty string.
     *  @package soset
     */
    function getKeysAsString($Delim = ' ', $Start = 0, $End = -1) {
        $result = $this->getKeys($Start, $End);
        if (count($result)) {
            return implode($Delim, $result);
        } else {
            return '';
        };
    } // getKeysAsString


    /** Joins an array to the set.
     *  Joins an array to this set. Array posno. or posname is used as a key.
     *  Example: joinArray(array("this"=>1, "that"=>2) would try to add keys "this"
     *      with value 1 and "that" with value 2 to the set. If they already
     *      exists, then they are skipped (not replaced);
     *  Example: joinArray(array("a", "b"), 1) would try to add keys "0" with
     *      value "a" and "1" with value "b". If they already exists in the set,
     *      then they are replaced with these new values.
     *  @param int $InArray Array to join.
     *  @param bool $Replace When set, existing items in this set are replaced
     *      with a value in join array.
     *  @return int Returns the number of the items inserted. That may be different
     *      from the array count when Replace is not set.
     */
    function joinArray($InArray, $Replace = 0) {
        if ((gettype($InArray) != 'array') || 
            count($InArray) == 0) return 0; // <-- EXIT

        $result = 0;
        $keys = array_keys($InArray);
        $kcount = count($keys);
        for ($k = 0; $k < $kcount; $k++) {
            if (($Replace) ||
                (!in_array($keys[$k], $this->Keys))) {
                $this->setItem($keys[$k], $InArray[$keys[$k]]);
                $result++;
            };
        } // for $k

        return $result;
    } // joinArray


    /** Imports an array as key values.
     *  Joins values of specified array to this set and assigns them to "".
     *  @param array/string $InArray Array that contains key names.
     *  @param bool $Replace When set(1), causes all existing items in this set to be
     *  replaced with an empty string.
     *  @return int Returns the number of the items inserted. That may be different
     *  	from the array count when Replace is not set.
     *  @package soset
     */
    function joinArrayAsKeys($InArray, $Replace = 0) {

        $result = 0;
        $keys = array_keys($InArray);
        $kcount = count($keys);
        for ($k = 0; $k < $kcount; $k++) {
            if (($Replace) ||
                (soIsNull(array_search($InArray[$keys[$k]], $this->Keys)))) {
                $this->setItem($InArray[$keys[$k]], '');
                $result++;
            };
        } // for $k

        return $result;
    } // joinArrayAsKeys


    /** Joins an array of "key=value" pairs.
     *  This method is basically here only due to backward compatibility
     *  reasons only, as it seems to be used widely everywhere. However,
     *  this should be get rid off as soon as possible. This call can be
     *  replaced by joinArray(soStrToAArray($InArray, $Delim, $Replace)).
     *
     *  @param array/string $InArray Array of "key=value" pairs.
     *  @param string $Delim A string to delimit key from value (e.g. "=")
     *  @param bool $Replace When set(1), the keys already in the set are replaced with
     *      the new value, otherwise when ont set(0), existing keys are skipped --
     *      in other words not replaced.
     *  @return int Returns the number of values added to the set or -1 on error such as
     *      non-array passed as input array or delimiter == "".
     */
    function joinKeyValueArray($InArray, $Delim = '=', $Replace = 0) {
        $InArray = soStrToAArray($InArray, $Replace, $Delim);
        if ($InArray == NULL) {
            return 0;
        } else {
            return $this->joinArray($InArray);
        }; // if
    } // joinKeyValueArray


    /** Get the set as an array.
     *  Returns the set as an numbered or associated array.
     *  @param bool $Numbered When set, returns an index numbered array (0, 1, 2),
     *      otherwise returns an associated array (array["key"] = value)
     *  @return array/any Returns the set as an array of keys and values
     *  	Array[key]=value; key may be either number or key name.
     *  @package soset
     */
    function getAsArray($Numbered = 0) {
        $result = array();
        $kcount = count($this->Keys);
        for ($k = 0; $k < $kcount; $k++) {
            if ($Numbered) {
                $key = $k;
            } else {
                $key = $this->Keys[$k];
            };
            $result[$key] = $this->Values[$k];
        };

        return $result;
    } // getAsArray


    /** Deletes an item.
     *  Removes an item by specified key.
     *  @param string $Key Key to item to delete.
     *  @return bool  Returns true(1) on success or false(0) when the key is not found.
     *  @package soset
     */
    function delItem($Key) {
        settype($Key, "string");
        $pos = $this->_getItemPos($Key);
        
        if ($pos != -1) {
            soDelElement($this->Keys, $pos);
            soDelElement($this->Values, $pos);
            $result = 1;
        } else {
            $result = 0;
        }; // if
    
        return $result;
    } // delItem
  

    /** Checks for an item.
     *  @param string $Key Key to item.
     *  @return bool Returns true(1) if an item by specified key exists,
     *  otherwise returns false(0).
     *  @package soset
     */
    function itemExists($Key) {
        settype($Key, "string");
        return in_array($Key, $this->Keys);
    } // itemExists


    /** Returns the item count.
     *  @return int Returns the number of items in the set.
    *  @package soset
     */
    function size() {
        return count($this->Keys);
    } // size


    /** Clears the set.
     *  Empties the whole set from value and keys.
     *  @package soset
     */
    function clear() {
        $this->Keys = array();
        $this->Values = array();
    } // clear


    /** Get the set as a string.
     *  Returns the set as a string delimited by specified item delimiter.
     *  @param bool $WithItemID When set, each item value is preceeded by item
     *      ID.
     *  @param string $EntryDelim Entry delimiter to delimit one element from
     *      another.
     *  @param string $KeyValueDelim A string to delimit key from value.
     *  @return string The whole set as a string.
     *  @package soset
     */
    function toString($WithItemName = 1, $ItemDelim = "\n", 
                      $KeyValueDelim = ' = ') {
        $keys = $this->getKeys();
        $kcount = count($keys);
        $result = '';
        $item_name = '';
        for ($k = 0; $k < $kcount; $k++) {
            if ($WithItemName) $item_name = $this->Keys[$k].$KeyValueDelim;
            $value = $this->Values[$k];
            if (gettype($value) == 'array') {
                $value = '<<'.soDumpArray($value).'>>';
            }; // if
            $result = soJoinStr($result, $ItemDelim,
                                $item_name.$value, 1);
        }; // for $k
        return $result;
    } // toString


    /** Returns the whole set as a string.
     *  Returns the whole set in a form of "key = 'value'". Each key-value
     *  pairs are delmited by specified delimiter string.
     *  For debuging purposes only.
     *  @param string $Delim String to delimit key=value pairs from each other
     *  @return string Dump string of the set.
     *  @package soset
     */
    function dump($Delim = "\n") {
        return $this->toString(1, $Delim, ' = ');
    } // dump


    /** Get element position no. in the set by key.
     *  @param string $Key Name of the item.
     *  @return any Returns item by specified key or an empty string if no such item
     *      exists (also Error property is set to true(1).
     *  @package soset
     */
    function _getItemPos($Key) {
        settype($Key, "string");
        $result = array_search($Key, $this->Keys, 1);
        if ($this->Debug) {
            soDebug('soSet->_getItemPos('.$Key.'): found "'.$result.'"', 
                    SOD_DET);
        };
        if (soIsNull($result)) $result = -1;

        return $result;
    } // _getitemPos


    /** Encodes the set to a URL string.
     *  Encodes the set so that each element are URL encoded and returns them
     *  as a string where each element is delimited by delimiter string.
     *  @param string $Delim Delimiter string to delimit each element from
     *      another. When specifying $Delim, make sure it is one of the special
     *      URL coded characters such as "&", so that it makes a difference as
     *      a delimiter.
     *  @param atring $EqualSign String to be used instead of default equal
     *      sign between key and value.
     *  @return string URL encoded set.
     *  Example:  if the set has items a=";" and b="aA", then encode("&") would
     *      return "a=%3B&b=aA".
     *  @package soset
     */
    function encode($Delim = '&', $EqualSign = '=') {
        $result = '';

        $icount = count($this->Keys);
        for ($i = 0; $i < $icount; $i++) {
            $result = soJoinStr($result, $Delim, $this->Keys[$i].$EqualSign.
                                urlencode($this->Values[$i]), 1);
        }; // for

        return $result;
    } // encode


    /** Decodes a URL encoded set back from string.
     *  Decodes each special delimiter delimited sub-string in input string
     *  and imports them to the set. 
     *  Example:  decode("a=%3B&b=aA", "&", 1) would import a set of a=";" and
     *      b="aA".
     *  @param string $InStr URL string to decode.
     *  @param string $Delim Delimiter used in the input string to delimit items from
     *      another.
     *  @param bool $Replace When set(1), all existing items are replaced by
     *      new, otherwise imported buplicates are skipped.
     *  @param atring $EqualSign String to be used instead of default equal
     *      sign between key and value.
     *  @package soset
     */
    function decode($InStr, $Delim = '&', $Replace = 1, $EqualSign = '=') {
        $icount = soValCount($InStr, $Delim);

        for ($i = 0; $i < $icount; $i++) {
            $split = soSplitStr(soExtractVal($InStr, $Delim, $i +1),
                                $EqualSign, 1, 0);
            if (count($split) && $split[0]) {
                $this->setItem($split[0], urldecode($split[1]), $Replace);
            };
        }; // for
    } // decode

}; // soSet


/** Extracts part of the items from a set or arrayset.
 *  @param soSet|soArraySet $Set ArraySet or Set to extract items from.
 *  @param array/string $Keys An array of item keys to extract.
 *  @param bool $Delete When set, deletes extracted item from the set.
 *  @return soSet|soArraySet/any Extracted items or NULL for invalid input set.
 *  @package soset
 */
function soExtractFromSet(&$Set, $Keys, $Delete = 0) {
    $type = soGetClass($Set);
    switch ($type) {
        case 'soset':
            $result = new soSet();
            break;
        case 'soarrayset':
            $result = new soArraySet();
            break;
        default:
            return NULL; // <-- EXIT
    }; // switch

    $kcount = count($Keys);
    for ($k = 0; $k < $kcount; $k++) {
        $result->addItem($Keys[$k], $Set->getItem($Keys[$k]));
        if ($Delete) $Set->delItem($Keys[$k]);
    }; // for $k

    return $result;
}; // soExtractFromSet


/* Test block
$set = new soSet();
$set->additem("3700", "jee");
echo $set->_getItemPos(3700)."\n";
 */

?>
